當您開始使用依賴注入 (Dependency Injection, DI) 時,一定會對 @Autowired 的便利性感到驚艷。然而,當一個介面 (Interface) 有多個實作類別 (Implementation Class) 時,Spring Boot 就會陷入「選擇困難症」。
今日來了解如何使用 @Primary 與 @Qualifier 這兩個強大的註解 (Annotation),優雅地解決這個問題。
NoUniqueBeanDefinitionException想像一個情境:我們正在開發一個訊息通知系統,需要支援 Email 和簡訊 (SMS) 兩種通知方式。
首先,定義一個 NotificationService 介面:
NotificationService.java
public interface NotificationService {
String send(String message);
}
接著,我們建立兩個實作類別 EmailServiceImpl 和 SmsServiceImpl,並將它們註冊為 Spring 的元件 (Bean)。
EmailServiceImpl.java
import org.springframework.stereotype.Service;
@Service("emailNotification") // 給這個 Bean 一個明確的名字 "emailNotification"
public class EmailServiceImpl implements NotificationService {
@Override
public String send(String message) {
System.out.println("正在透過 Email 發送...");
return "Email sent with message: " + message;
}
}
SmsServiceImpl.java
import org.springframework.stereotype.Service;
@Service("smsNotification") // 同樣地,給它一個名字 "smsNotification"
public class SmsServiceImpl implements NotificationService {
@Override
public String send(String message) {
System.out.println("正在透過簡訊發送...");
return "SMS sent with message: " + message;
}
}
現在,問題來了。當我們在 Controller 中嘗試注入 NotificationService 時:
NotificationController.java (錯誤的範例)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
private final NotificationService notificationService;
@Autowired // 錯誤發生點!
public NotificationController(NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/notify")
public String sendNotification() {
return notificationService.send("Hello, World!");
}
}
當您啟動應用程式時,Spring 會在終端機噴出一段錯誤訊息,其中最關鍵的一行是 NoUniqueBeanDefinitionException。
為什麼會出錯?
Spring 發現有兩個符合 NotificationService 型別的 Bean (emailNotification 和 smsNotification),它不知道您到底想要哪一個,於是只好放棄並報錯。
接下來,我們來看看如何解決這個問題。
@Primary - 設定預設選項@Primary 是最簡單的解決方案。它就像是告訴 Spring:「如果有多個選擇,而且沒有人特別指定要哪一個時,請優先選擇我!」
這好比餐廳套餐裡的「預設飲料」是紅茶,您不特別說,店家就自動給您紅茶。
讓我們將 EmailServiceImpl 設為預設的通知方式:
EmailServiceImpl.java (使用 @Primary)
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@Service("emailNotification")
@Primary // 就是這一行!告訴 Spring 這是首選
public class EmailServiceImpl implements NotificationService {
@Override
public String send(String message) {
System.out.println("正在透過 Email 發送...");
return "Email sent with message: " + message;
}
}
現在,您不需要修改 NotificationController 的任何程式碼,直接重新啟動應用程式,它就能正常運作了!Spring 會因為 @Primary 的存在,自動注入 EmailServiceImpl。
@Qualifier - 精準指定@Primary 很好用,但如果我們在某個地方不想用預設的 Email,而是想用簡訊 (SMS) 來發送通知呢?這時 @Qualifier 就要登場了。
@Qualifier 的作用就像是「指定點餐」。您明確告訴 Spring:「我不要預設的,我指定要名稱為『這個』的 Bean。」
@Qualifier 如何對應到 Bean?@Qualifier 是透過 Bean 的名稱 來識別的。這個名稱有兩種來源:
@Service("smsNotification"),我們手動將 Bean 命名為 smsNotification。SmsServiceImpl 的預設 Bean 名稱就是 smsServiceImpl。@Qualifier 的三種注入方法比較現在,讓我們修改 Controller,明確指定要注入 smsNotification。
將 @Qualifier 放在建構子的參數前面。
NotificationController.java (修正後)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; // 記得 import
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NotificationController {
private final NotificationService notificationService;
@Autowired
public NotificationController(@Qualifier("smsNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
@GetMapping("/notify")
public String sendNotification() {
return notificationService.send("這是一則來自 SMS 的重要通知!");
}
}
在這個例子中,即使 EmailServiceImpl 標有 @Primary,Spring 也會聽從 @Qualifier 的指令,精準地注入 SmsServiceImpl。
雖然方便,但因其缺點(不易測試、隱藏依賴),較不建議在正式專案中使用。
@RestController
public class NotificationController {
@Autowired
@Qualifier("smsNotification")
private NotificationService notificationService;
// ...
}
適用於可選的依賴,或解決循環依賴問題。
@RestController
public class NotificationController {
private NotificationService notificationService;
@Autowired
public void setNotificationService(@Qualifier("smsNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
// ...
}
@Primary vs @Qualifier 大對決讓我們用生動的「餐廳點餐比喻」來做個總結,幫助您徹底理解兩者的區別。
想像您走進一家餐廳 (Spring 容器),餐廳提供多種飲料 (多個同型別的 Bean)。
@Primary = 餐廳的「今日推薦」@Autowired 但未指定)@Primary 的那個 Bean。它是一個被動的預設選項。@Qualifier = 您「指定點餐」@Qualifier("lemonTea"))@Qualifier 是一個主動的、精確的指令。| 情境 | Spring 對應 | 行為 |
|---|---|---|
| 沒特別說 → 上今日推薦 | 只有 @Autowired,且其中一個 Bean 有 @Primary |
注入標有 @Primary 的 Bean |
| 沒特別說 → 服務生困惑 | 只有 @Autowired,沒有 @Primary,但有多個選項 |
拋出 NoUniqueBeanDefinitionException 錯誤 |
| 指定要檸檬紅茶 | 使用 @Autowired 搭配 @Qualifier("lemonTea") |
精準注入名為 "lemonTea" 的 Bean |
| 今日推薦是奶茶,但我指定要檸檬紅茶 | @Primary 在 milkTea 上,但注入點使用 @Qualifier("lemonTea") |
@Qualifier 優先級更高,注入 "lemonTea" |